package ru.ipo.dces;

import ru.ipo.dces.client.Controller;
import ru.ipo.dces.clientservercommunication.DownloadPluginRequest;
import ru.ipo.dces.clientservercommunication.DownloadPluginResponse;
import ru.ipo.dces.clientservercommunication.PluginSide;
import ru.ipo.dces.exceptions.GeneralRequestFailureException;
import ru.ipo.dces.exceptions.ServerReturnedError;
import ru.ipo.dces.server.ServerFacade;
import ru.ipo.dces.server.ServerFailReason;
import ru.ipo.dces.utils.FileSystemUtils;
import ru.ipo.dces.utils.IOUtils;
import ru.ipo.dces.utils.JarJarURLConnection;
import ru.ipo.dces.utils.ResponseHandler;

import javax.swing.*;
import java.io.*;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.*;

/**
 * Created by IntelliJ IDEA.
 * User: ilya
 * Date: 05.05.11
 * Time: 12:49
 */
public class PluginsRegistry {

    private final File pluginsFolder;
    private final ServerFacade pluginsServer;
    private final ArrayList<String> pluginsPackages = new ArrayList<String>();
    private boolean loadTemporaryFiles = true;

    private final Map<Class<?>, HashMap<String, Class<?>>> class2id2pluginClass = new HashMap<Class<?>, HashMap<String, Class<?>>>();

    static {
        //TODO get rid of JarJar, i.e. don't use dependencies folder
        JarJarURLConnection.register();
    }

    public PluginsRegistry(File pluginsFolder, ServerFacade pluginsServer) {
        if (pluginsFolder != null) {
            if (!pluginsFolder.exists())
                pluginsFolder.mkdirs();
            if (!pluginsFolder.isDirectory())
                throw new IllegalArgumentException("Plugins folder should be a directory");
            this.pluginsFolder = pluginsFolder.getAbsoluteFile();
        } else
            this.pluginsFolder = null;
        this.pluginsServer = pluginsServer;
    }

    public PluginsRegistry(File pluginsFolder) {
        this(pluginsFolder, null);
    }

    public PluginsRegistry() {
        this(null, null);
    }

    public PluginsRegistry(File pluginsFolder, ServerFacade pluginsServer, String... packages) {
        this(pluginsFolder, pluginsServer);
        pluginsPackages.addAll(Arrays.asList(packages));
    }

    public void addPluginsPackage(String packageName) {
        pluginsPackages.add(packageName);
    }

    public void removePluginsPackage(String packageName) {
        pluginsPackages.remove(packageName);
    }

    //TODO add getPluginAsynchronous method (!)
    //TODO may be remove getPlugin and rename *Async to getPlugin

    public <T> Class<? extends T> getPlugin(Class<T> pluginBaseClass, String id) throws IOException, ClassNotFoundException, GeneralRequestFailureException, ServerReturnedError {
        //search in map
        HashMap<String, Class<?>> id2pluginClass = class2id2pluginClass.get(pluginBaseClass);

        //create map entry if necessary
        if (id2pluginClass == null) {
            id2pluginClass = new HashMap<String, Class<?>>();
            class2id2pluginClass.put(pluginBaseClass, id2pluginClass);
        }

        Class<?> pluginClass = id2pluginClass.get(id);
        if (pluginClass == null) {
            pluginClass = loadClass(pluginBaseClass, id);
            id2pluginClass.put(id, pluginClass);
        }

        return pluginClass.asSubclass(pluginBaseClass);
    }

    public boolean getLoadTemporaryFiles() {
        return loadTemporaryFiles;
    }

    public void setLoadTemporaryFiles(boolean loadTemporaryFiles) {
        this.loadTemporaryFiles = loadTemporaryFiles;
    }

    public void clear() {
        class2id2pluginClass.clear();
    }

    private <T> Class<? extends T> loadClass(Class<T> pluginBaseClass, String id) throws IOException, ServerReturnedError, GeneralRequestFailureException, ClassNotFoundException {
        Class<? extends T> pluginClass;

        //first search in packages
        pluginClass = loadClassFromPackages(pluginBaseClass, id);
        if (pluginClass != null)
            return pluginClass;

        //do nothing if there is no plugin folder
        if (pluginsFolder == null)
            return null;

        //search plugin in plugins folder
        File pluginFile = new File(pluginsFolder.getPath() + File.separatorChar + id + ".jar");
        if (!pluginFile.exists())
            if (pluginsServer != null)
                downloadPluginFromServer(id, pluginFile);
            else
                throw new FileNotFoundException("File for plugin with id " + id + " not found");

        if (loadTemporaryFiles) {
            File temporaryPluginFile = File.createTempFile("plugin_" + id, ".jar");
            FileSystemUtils.copyFile(pluginFile, temporaryPluginFile);
            pluginFile = temporaryPluginFile;
        }

        return loadClassFromFile(pluginBaseClass, pluginFile);
    }

    private <T> Class<? extends T> loadClassFromPackages(Class<T> pluginBaseClass, String id) {
        for (String pluginsPackage : pluginsPackages) {
            try {
                return Class.forName(pluginsPackage + "." + id).asSubclass(pluginBaseClass);
            } catch (ClassNotFoundException e) {
                //do nothing, just continue search
            }
        }

        return null;
    }

    private <T> Class<? extends T> loadClassFromFile(Class<T> pluginBaseClass, File pluginFile) throws IOException, ClassNotFoundException {
        ClassLoader pluginClassLoader = getPluginClassLoader(pluginFile);
        return findMainPluginClass(pluginFile, pluginClassLoader, pluginBaseClass);
    }

    private void downloadPluginFromServer(String id, final File pluginFile) throws IOException, GeneralRequestFailureException, ServerReturnedError {
        //load file from server
        final DownloadPluginRequest installRequest = new DownloadPluginRequest();
        //TODO now installs only client plugins
        installRequest.side = PluginSide.Client;
        installRequest.pluginAlias = id;
        ResponseHandler<DownloadPluginResponse> handler = new ResponseHandler<DownloadPluginResponse>() {
            @Override
            public void success(DownloadPluginResponse installResponse) {
                //write PluginFile to temporary location
                FileSystemUtils.saveBytesToFile(installResponse.pluginBytes, pluginFile);
            }

            @Override
            public void fail(ServerFailReason exception) {
            }
        };
        Controller.doRequest(installRequest, handler, "Загрузка модуля решения задач с сервера");
    }

    private ClassLoader getPluginClassLoader(File pluginFile) throws IOException {
        JarInputStream jf = null;
        try {
            jf = new JarInputStream(new FileInputStream(pluginFile));
            ArrayList<URL> urls = new ArrayList<URL>();
            URL pluginURL = pluginFile.toURI().toURL();
            urls.add(pluginURL);

            //find all dependencies
            JarEntry jarEntry;
            while ((jarEntry = jf.getNextJarEntry()) != null) {

                String jarName = jarEntry.getName();

                if (jarName.startsWith("dependencies/") && jarName.endsWith(".jar")) {
                    urls.add(new URL("jar:jarjar:" + pluginURL + "%/" + jarName + "!/"));
                }
            }

            return new PatchedURLClassLoader(urls.toArray(new URL[urls.size()]));
        } finally {
            IOUtils.closeQuietly(jf);
        }
    }

    private <T> Class<? extends T> findMainPluginClass(File pluginFile, ClassLoader classLoader, Class<T> pluginClass) throws ClassNotFoundException, IOException {
        JarInputStream jf = null;
        try {
            jf = new JarInputStream(new FileInputStream(pluginFile));

            Manifest manifest = getManifest(jf); //jf.getManifest() doesn't work if the manifest is not in the first entry
            Attributes mainAttributes = manifest.getMainAttributes();
            String value = mainAttributes.getValue("DCES-Main-Class");
            return classLoader.loadClass(value).asSubclass(pluginClass);
        } finally {
            IOUtils.closeQuietly(jf);
        }
    }

    private Manifest getManifest(JarInputStream jf) throws IOException {
        Manifest man = jf.getManifest();
        if (man != null)
            return man;

        JarEntry entry;
        while ((entry = jf.getNextJarEntry()) != null) {
            if (entry.getName().equalsIgnoreCase(JarFile.MANIFEST_NAME)) {
                man = new Manifest();
                man.read(new BufferedInputStream(jf)); //TODO why this line is commented in JarInputStream sources??
                jf.close();
                return man;
            }
        }

        return null;
    }

    private class PatchedURLClassLoader extends URLClassLoader {

        public PatchedURLClassLoader(URL[] urls) {
            super(urls);
        }

        @Override
        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//            System.out.println("LOADES CLAZZ " + name + " " + resolve);
            if ("javax.swing.UIManager".equals(name))
                return UIManager.class;

            return super.loadClass(name, resolve);
        }

    }

}
